﻿using Entities;
using Entities.Cities;
using Logger4Net;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Utilities;
using System.IO;
using Antlr.Runtime;

namespace Hubberly
{
   public partial class Main : Form
   {
      //////////////////////////////////////////////////////////////////////////
      //
      public Main()
      {
         InitializeComponent();
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void Main_Load(object sender, EventArgs e)
      {
         try
         {
            mruFile_ = new Utilities.MRUFile();
            mruFile_.config = Program.settings.mru;
            mruFile_.eventClicked += new Utilities.MRUFile.ClickedEventHandler(handleClicked);
            mruFile_.fileMenu = fileToolStripMenuItem;

            initializeListViewGroups();
            initializeListViewCities();
            initializeProductionTickets();

            cbProdShowGoods.Items.AddRange(showGoods_);
            cbProdShowGoods.SelectedItem = cbProdShowGoods.Items.OfType<ShowGoodsItem>().
               FirstOrDefault(item => item.showGoods == Program.settings.showGoods);
            maskProdPopulation.eventChanged += new MaskUpDown.ChangedHandler(handleChanged);

            fillPortsForSelectedConvoy();
            checkConvoyButtons();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void resyncWorldInfo()
      {
         string filename = string.Empty;
         try
         {
            logger_.debug("resyncWorldInfo()");

            filename = Program.settings.worldFilename;
            if (!File.Exists(filename ?? string.Empty))
            {
               return;
            }

            // Collect all of the information from worldinfo.txt
            WorldInfoData infoFile = new WorldInfoData(world_);

            // The parser throws an exception if there's a problem

            // parse the file
            string allFile = File.ReadAllText(filename, Encoding.Unicode);
            MemoryStream stream = new MemoryStream(new System.Text.UnicodeEncoding().GetBytes(allFile));
            ANTLRInputStream input = new ANTLRInputStream(stream, Encoding.Unicode);
            WorldInfoFileLexer lexer = new WorldInfoFileLexer(input);
            CommonTokenStream tokens = new CommonTokenStream(lexer);
            WorldInfoFileParser parser = new WorldInfoFileParser(tokens);
            parser.world = world_;
            parser.info = infoFile;

            logger_.debugFormat("parsing worldinfo file '{0}'", filename);
            parser.parseWorld();
            if (parser.Failed)
            {
               logger_.errorFormat("parsing failed");
            }

            // That seemed to parse OK, so update to the latest information
            logger_.debugFormat("importing {0} cities", infoFile.infoCities.Count());
            infoFile.infoCities.ForEach(infoCity =>
               {
                  City city = infoCity.city;
                  city.population = infoCity.population;
                  infoCity.factories.All(kvp =>
                     {
                        city.factories[kvp.Key] = kvp.Value;
                        return true;
                     });
               });

            logger_.debugFormat("importing {0} convoys", infoFile.infoConvoys.Count());

            // Preserve the hubs, if we can
            City hubCity;
            Dictionary<string, City> hubs = new Dictionary<string, City>();
            world_.convoys.Values.All(convoy =>
               {
                  if (null != (hubCity = convoy.hub))
                  {
                     hubs.Add(convoy.name, hubCity);
                  }
                  return true;
               });
            world_.convoys.Clear();
            infoFile.infoConvoys.All(kvp =>
               {
                  try
                  {
                     // If this object has ports, then it is a convoy definition
                     if (0 < kvp.Value.Count())
                     {
                        Convoy convoy = new Convoy(world_);
                        convoy.name = kvp.Key;
                        kvp.Value.ForEach(port => convoy.addPort(port));

                        if (hubs.TryGetValue(convoy.name, out hubCity) &&
                           convoy.snapshotPorts().Contains(hubCity))
                        {
                           // We already have a hub for a convoy with this name
                           // (i.e. no change since we last resynced)
                           convoy.hub = hubCity;
                        }
                     }
                     return true;
                  }
                  catch (System.Exception ex)
                  {
                     logger_.exception("adding {0} ports to convoy {1}\n", ex, kvp.Value.Count(), kvp.Key);
                  }
                  return false;
               });

            // And recalculate the world from the new information
            world_.recalculate();
            refreshConvoyCombo();
            rebuildConvoyTab();
            checkDirty();
            return;
         }
         catch (RecognitionException ex)
         {
            logger_.exception("RecognitionException: '{0}' at approx col {1} of line {2}", ex, (char)ex.Character, ex.CharPositionInLine, ex.Line);
         }
         catch (System.Exception ex)
         {
            logger_.exception("failed to resync world info from '{0}'\n", ex, filename);
         }
         logger_.errorFormat("failed to resync world info from '{0}'", filename);
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void Main_Shown(object sender, EventArgs e)
      {
         try
         {
            handleResize(lvProdCities, null);
            handleResize(lvConvoyCities, null);
            handleResize(lvConvoyPorts, null);

            lvProdCities.Items[0].Selected = true;
            if (0 < lvConvoyCities.Items.Count)
            {
               lvConvoyCities.Items[0].Selected = true;
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void handleResize(object sender, EventArgs e)
      {
         try
         {
            // If it is a list view with one column, then we size the column
            // to fill the grid
            ListView view = sender as ListView;
            if ((null != view) && (1 == view.Columns.Count))
            {
               view.Columns[0].Width = view.ClientSize.Width;
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void handleSelectedIndexChanged(object sender, EventArgs e)
      {
         try
         {
            if (sender == lvProdCities)
            {
               refreshConvoyCombo();
            }
            else if (sender == cbProdConvoy)
            {
               refreshProductionTab();
            }
            else if (sender == cbProdShowGoods)
            {
               Program.settings.showGoods = (cbProdShowGoods.SelectedItem as ShowGoodsItem).showGoods;
               refreshProductionTab();
            }
            else if (sender == lvConvoyNames)
            {
               fillPortsForSelectedConvoy();
            }
            else if (sender == lvConvoyPorts)
            {
               checkConvoyButtons();
            }
            else if (sender == lvConvoyCities)
            {
               checkConvoyButtons();
            }
            else if (sender == cbConvoyHub)
            {
               if (lvConvoyNames.SelectedItems.OfType<ListViewItem>().All(item =>
                  {
                     (item.Tag as Convoy).hub = cbConvoyHub.SelectedItem as City;
                     return true;
                  }))
               {
                  checkDirty();
               }
            }
            else
            {
               logger_.errorFormat("Unknown output type '{0}' in handleSelectedIndexChanged()",
                  sender.GetType().FullName);
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void fillPortsForSelectedConvoy()
      {
         try
         {
            //logger_.debug("fillPortsForSelectedConvoy()");

            lvConvoyNames.Enabled = (0 < lvConvoyNames.Items.Count);
            Convoy convoy = selectedConvoy;
            if (null == convoy)
            {
               lvConvoyPorts.Items.Clear();
               cbConvoyHub.Items.Clear();
               btnRemoveConvoy.Enabled = lvConvoyCities.Enabled =
                  lvConvoyPorts.Enabled = lblConvoyHub.Enabled =
                  cbConvoyHub.Enabled = false;
            }
            else
            {
               btnRemoveConvoy.Enabled = lvConvoyCities.Enabled =
                  lvConvoyPorts.Enabled = lblConvoyHub.Enabled =
                  cbConvoyHub.Enabled = true;
               convoy.snapshotPorts().ForEach(city =>
               {
                  ListViewItem item = new ListViewItem();
                  item.Text = city.name;
                  item.Tag = city;
                  lvConvoyPorts.Items.Add(item);

                  int index = cbConvoyHub.Items.Add(city);
                  if (convoy.hub == city)
                  {
                     cbConvoyHub.SelectedItem = city;
                  }
               });
            }
            checkConvoyButtons();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void handleClick(object sender, EventArgs e)
      {
         try
         {
            logger_.debug("handleClick()");

            if (sender == btnPortUp)
            {
               Convoy convoy = selectedConvoy;
               lvConvoyPorts.SelectedIndices.OfType<int>().All(index =>
               {
                  if (0 != index)
                  {
                     ListViewItem item = lvConvoyPorts.Items[index];
                     City city = item.Tag as City;

                     convoy.removePort(index);
                     convoy.insertPort(index - 1, city);
                     convoy.dirty = true;
               
                     lvConvoyPorts.Items.RemoveAt(index);
                     lvConvoyPorts.Items.Insert(index - 1, item);

                     // Keep it selected in case we are moving more than one step
                     item.Selected = true;
                  }
                  return true;
               });
            }
            else if (sender == btnPortDown)
            {
               Convoy convoy = selectedConvoy;
               int maxIndex = lvConvoyPorts.Items.Count - 1;
               lvConvoyPorts.SelectedIndices.OfType<int>()
                  .Reverse<int>().All(index =>
                  {
                     if (index != maxIndex)
                     {
                        ListViewItem item = lvConvoyPorts.Items[index];
                        City city = item.Tag as City;

                        convoy.removePort(index);
                        convoy.insertPort(index + 1, city);
                        convoy.dirty = true;

                        lvConvoyPorts.Items.RemoveAt(index);
                        lvConvoyPorts.Items.Insert(index + 1, item);

                        // Keep it selected in case we are moving more than one step
                        item.Selected = true;
                     }
                     return true;
                  });
            }
            else if ((sender == btnRemovePort) || (sender == lvConvoyPorts /* DoubleClick */))
            {
               Convoy convoy = selectedConvoy;
               lvConvoyPorts.SelectedIndices.OfType<int>()
                  .Reverse<int>().All(index =>
                  {
                     City port = lvConvoyPorts.Items[index].Tag as City;
                     lvConvoyPorts.Items.RemoveAt(index);
                     convoy.removePort(index);
                     convoy.dirty = true;

                     if (port == convoy.hub)
                     {
                        // If that was the only occurrence of this city in the convoy,
                        // then it can no longer be the hub
                        if (!lvConvoyPorts.Items.OfType<ListViewItem>()
                           .Any(item => (item.Tag as City) == port))
                        {
                           cbConvoyHub.Items.Remove(port);
                           convoy.hub = null;
                        }
                     }
                     return true;
                  });
               refreshConvoyCombo();
            }
            else if ((sender == btnAddPort) || (sender == lvConvoyCities /* DoubleClick */))
            {
               Convoy convoy = selectedConvoy;
               lvConvoyCities.SelectedIndices.OfType<int>().All(index =>
                  {
                     ListViewItem item = lvConvoyCities.Items[index];
                     City city = item.Tag as City;

                     item = new ListViewItem(city.name);
                     item.Tag = city;
                     lvConvoyPorts.Items.Add(item);
                     convoy.addPort(city);
                     convoy.dirty = true;

                     if (!cbConvoyHub.Items.Contains(city))
                     {
                        cbConvoyHub.Items.Add(city);
                     }
                     return true;
                  });
               refreshConvoyCombo();
            }

            checkConvoyButtons();
            checkDirty();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      void handleSelectAllProducers(object sender, TradeTicket.SelectAllProducersEventArgs eventArgs)
      {
         try
         {
            logger_.debug("handleSelectAllProducers()");

            lvProdCities.Items.OfType<ListViewItem>()
               .Where(item => (item.Tag as City).factories.Keys.Any(factory => factory.output == eventArgs.material))
               .ToList().ForEach(item => item.Selected = true);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      void handleFactoriesChanged(object sender, TradeTicket.FactoriesChangedEventArgs eventArgs)
      {
         try
         {
            logger_.debug("handleFactoriesChanged()");

            // Get the subset of the selected cities that can have factories that output the given material
            bool changed = false;
            selectedCities.ForEach(city =>
               {
                  if (city.updateFactoryCount(eventArgs.material.producer, eventArgs.factoryCount))
                  {
                     changed = true;
                  }
                  if ((0 <= eventArgs.utilization) && city.updateUtilization(eventArgs.material.producer, eventArgs.utilization))
                  {
                     changed = true;
                  }
               });

            if (changed)
            {
               // Rebuild the dependencies to take account of this change
               world_.recalculate();
               refreshProductionTab();
               checkDirty();
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void exitToolStripMenuItem_Click(object sender, EventArgs e)
      {
         Close();
      }

      //////////////////////////////////////////////////////////////////////////
      //
      void handleChanged(object sender, MaskUpDown.ChangedEventArgs eventArgs)
      {
         try
         {
            logger_.debug("handleChanged()");

            MaskUpDown maskBox = (sender as MaskUpDown);
            if (maskProdPopulation == maskBox)
            {
               selectedCities[0].population = (int)eventArgs.newValue;
               world_.recalculate();
               refreshProductionTab();
               checkDirty();
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private List<City> selectedCities
      {
         get
         {
            return lvProdCities.SelectedItems.OfType<ListViewItem>().ToList().
               ConvertAll<City>(item => item.Tag as City);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void refreshProductionTab()
      {
         try
         {
            //logger_.debug("refreshProductionTab()");

            List<City> snapshotSelectedCities = selectedCities;
            if (0 == snapshotSelectedCities.Count)
            {
               return;
            }

            maskProdPopulation.Enabled = (1 == snapshotSelectedCities.Count);
            maskProdPopulation.maskBuilder = string.Concat("999999990 of ", Global.escapedString(world_.population));
            maskProdPopulation.currentValue = snapshotSelectedCities.Sum(city => city.population);

            tradeTickets_.ForEach(ticket => refreshTicket(snapshotSelectedCities, ticket));
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void refreshConvoyCombo()
      {
         try
         {
            //logger_.debug("refreshConvoyCombo()");

            // Put cities into the correct group according to the convoy definitions
            City city;
            lvProdCities.Items.OfType<ListViewItem>().All(item =>
            {
               city = item.Tag as City;
               item.Group = lvProdCities.Groups[city.hasConvoy ? groupInConvoy_ : groupNotSupplied_];
               return true;
            });

            // Fill the combo box with all of the convoys that include the selected cities
            cbProdConvoy.Items.Clear();
            cbProdConvoy.Enabled = false;
            if (0 < lvProdCities.SelectedItems.Count)
            {
               city = lvProdCities.SelectedItems[0].Tag as City;
               if (city.hasConvoy)
               {
                  // Check that all of the other cities have the same convoys
                  if (lvProdCities.SelectedItems.OfType<ListViewItem>()
                     .Select<ListViewItem, City>(item => item.Tag as City)
                     .Skip(1).All(qCity => qCity.getConvoysMatch(city)))
                  {
                     // That looks OK, so fill the combo box with the convoys
                     city.convoys.ForEach(convoy => cbProdConvoy.Items.Add(convoy));
                     cbProdConvoy.Enabled = true;
                     cbProdConvoy.SelectedIndex = 0;
                  }
               }
            }

            // Refresh the rest of the production tab
            refreshProductionTab();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void refreshTicket(List<City> snapshotSelectedCities, TradeTicket ticket)
      {
         try
         {
            //logger_.debugFormat("refreshTicket({0})", ticket.material.name);

            Material material = ticket.material;
            ticket.supplyHub = cbProdConvoy.SelectedItem as City;

            // The Factories box
            int factoryCount, citiesCanBuildFactory;
            double dailyProduction = ticketCountFactories(ticket, selectedCities, out factoryCount, out citiesCanBuildFactory);
            ticket.factoriesActual = factoryCount;

            // Should we be displaying this ticket?
            if (!ticketShowGoods(ticket, selectedCities, citiesCanBuildFactory))
            {
               return;
            }

            double worldProdDemand = world_.getProductionDemand(material);
            double worldPopDemand = world_.population * material.perPopulation;
            double cityProdDemand = snapshotSelectedCities.Sum(city => city.getProductionDemand(material));
            double cityPopDemand = snapshotSelectedCities.Sum(city => city.getPopulationDemand(material));

            // We allow the user to change the number iff there is exactly one city selected
            ticket.factoriesEnabled = (1 == snapshotSelectedCities.Count) && (1 == citiesCanBuildFactory);
            ticket.factoriesNeeded = (0 < material.count) ?
               (int)Math.Ceiling((worldProdDemand + worldPopDemand) / material.count) : -1;

            // The demand for this product
            int actualDemand = (int)Math.Ceiling(cityPopDemand + cityProdDemand - dailyProduction);
            int totalDemand = (int)Math.Ceiling(worldProdDemand + worldPopDemand);
            if (0 == totalDemand)
            {
               ticket.demandText = string.Empty;
            }
            else
            {
               string actualFormatted = actualDemand.ToString("--- #,0; +++ #,0;no change");
               ticket.demandText = string.Concat(actualFormatted, " of ", totalDemand.ToString("#,0"), "");
            }

            // The net production is the daily production less the actual demand
            ticket.dailyProductionText = (0 < citiesCanBuildFactory) ?
               Math.Max(0, dailyProduction - actualDemand).ToString("#,0.#") : string.Empty;

            // The adminBuy and adminSell boxes
            ticketShowQuantities(ticket, snapshotSelectedCities, cityPopDemand, dailyProduction, cityProdDemand);

            // Warehousing displays (this material in selected cities) of (all materials in selected cities)
            refreshTicketWarehouse(ticket, selectedCities);

            // Display the summary utilization for the factory that makes this
            bool allSame = false;
            string asString = Program.combine(out allSame, snapshotSelectedCities,
               city => (null != material.producer) ? city.getUtilization(material.producer).ToString() : "-1");
            int utilization = allSame ? int.Parse(asString) : -1;
            ticket.utilization = utilization;

            //dumpTicket(snapshotSelectedCities, ticket);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void ticketShowQuantities(TradeTicket ticket, List<City> cities,
         double cityPopDemand, double dailyProduction, double cityProdDemand)
      {
         try
         {
            //logger_.debugFormat("tickShowQuantities({0})", ticket.material.name);

            if (null == cbProdConvoy.SelectedItem)
            {
               switch (cbProdConvoy.Items.Count)
               {
                  case 0:
                     ticket.adminBuyText = ticket.adminSellText = "<no convoy>";
                     return;

                  case 1:
                     // This will trigger another refresh ...
                     cbProdConvoy.SelectedItem = cbProdConvoy.Items[0];
                     return;

                  default:
                     ticket.adminBuyText = ticket.adminSellText = "<choose convoy>";
                     return;
               }
            }

            Convoy convoy = cbProdConvoy.SelectedItem as Convoy;
            double journeyTime = convoy.roundTripTime;
            lblJourneyTime.Text = journeyTime.ToString("0.0");

            // If there is more than one city, then they all must be part of the same convoy
            // in order to display something meaningful
            List<City> ports = convoy.snapshotPorts();
            if ((0 == cities.Count()) || cities.Any(city => !ports.Contains(city)))
            {
               ticket.adminBuyText = ticket.adminSellText = "<select cities from the same convoy>";
               lblJourneyTime.Text = "---";
               cbProdConvoy.SelectedItem = null;
               return;
            }

            // The admin buy box covers the total demand for this city over the period between restocks
            cityPopDemand *= journeyTime;
            cityProdDemand *= journeyTime;
            dailyProduction *= journeyTime;
            double netProdDemand = cityProdDemand - dailyProduction;
            int actualDemand = (int)Math.Ceiling(cityPopDemand + netProdDemand);

            StringBuilder sb = new StringBuilder();

            if (0 < actualDemand)
            {
               sb.Append(cityPopDemand.ToString("0.0"));
               sb.Append((0 > netProdDemand) ? " - " : " + ");
               sb.Append(Math.Abs(netProdDemand).ToString("0.0"));
               sb.Append(" = ");
               sb.Append(actualDemand);
               sb.Append(" @ ");
               sb.Append(ticket.material.buyPrice);

            }
            else if (0 != dailyProduction)
            {
               sb.Append("locally sourced");
            }
            ticket.adminBuyText = sb.ToString();

            // The admin sell box is to cover the production demand for the factories that
            // we have in the selected cities.
            ticket.adminSellText = string.Concat((int)Math.Ceiling(cityProdDemand * journeyTime), " @ ", ticket.material.sellPrice);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private double ticketCountFactories(TradeTicket ticket, List<City> cities, out int factoryCount, out int citiesCanBuildFactory)
      {
         int canBuildCount = 0;
         double dailyProduction = 0;
         try
         {
            //logger_.debugFormat("ticketCountFactories({0})", ticket.material.name);

            Factory factory = ticket.material.producer;
            
            citiesCanBuildFactory = 0;
            factoryCount = cities.Sum(city =>
            {
               // The material on this ticket has a factory to make it
               int thisCount = 0;
               if ((null != factory) && city.factories.TryGetValue(factory, out thisCount))
               {
                  // And this city can build those factories
                  ++canBuildCount;

                  // The industry may be disabled
                  int factoryUtilization = city.getUtilization(factory);
                  if (0 < factoryUtilization)
                  {
                     dailyProduction += factoryUtilization * ticket.material.count * thisCount / 100.0;
                  }
               }
               return thisCount;
            });
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
            factoryCount = 0;
         }
         finally
         {
            citiesCanBuildFactory = canBuildCount;
         }
         return dailyProduction;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private bool ticketShowGoods(TradeTicket ticket, List<City> cities, int citiesCanBuildFactory)
      {
         //logger_.debugFormat("ticketShowGoods({0})", ticket.material.name);

         bool ticketVisible = false;
         try
         {
            ShowGoodsIndices showGoods = (cbProdShowGoods.SelectedItem as ShowGoodsItem).showGoods;
            switch (showGoods)
            {
               case ShowGoodsIndices.All:
                  ticketVisible = true;
                  break;

               case ShowGoodsIndices.FactoriesOnly:
                  ticketVisible = (0 != citiesCanBuildFactory);
                  break;

               case ShowGoodsIndices.ProductionChain:
                  ticketVisible = selectedCities.Any(city => city.getInProductionChain(ticket.material));
                  break;

               default:
                  throw new Exception("Unrecognized value for ShowGoodsIndices: " + showGoods.ToString());
            }
            ticket.Visible = ticketVisible;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         return ticketVisible;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void refreshTicketWarehouse(TradeTicket ticket, List<City> cities)
      {
         try
         {
            //logger_.debugFormat("refreshTicketWarehouse({0})", ticket.material.name);

            double warehouseAllMaterials = 0;
            double warehouseThisMaterial = cities.Sum(city =>
            {
               double result = 0;
               world_.almanac.materials.ForEach(M =>
               {
                  double qty = city.getWarehouse(M);
                  if (M == ticket.material)
                  {
                     result = qty;
                  }
                  warehouseAllMaterials += qty;
               });
               return result;
            });

            int roundedThese = (int)Math.Ceiling(warehouseThisMaterial);
            int roundedAll = (int)Math.Ceiling(warehouseAllMaterials);
            //roundedAll += warehouseCapacity_ - 1;
            //roundedAll -= roundedAll % warehouseCapacity_;

            string percent = string.Empty;
            if (0 != warehouseAllMaterials)
            {
               double fraction = warehouseThisMaterial / warehouseAllMaterials;
               percent = string.Concat(" (", fraction.ToString("#0%"), ")");
               ticket.warehouseText = string.Concat(roundedThese, " of ", roundedAll, percent);
            }
            else
            {
               ticket.warehouseText = string.Empty;
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void dumpTicket(List<City> cities, TradeTicket ticket)
      {
         try
         {
            int count = cities.Count;
            StringBuilder sb = new StringBuilder();
            
            sb.Append("ticket for ");
            if (0 == count)
            {
               sb.Append("no cities");
            }
            else
            {
               sb.Append(cities[0].name.Substring(0, 3));
               cities.Skip(1).All(city => null != sb.Append(",").Append(city.name.Substring(0, 3)));
            }
            sb.Append(": material[");
            sb.Append(ticket.material.name);
            
            sb.Append("], factories[");
            sb.Append(ticket.factoriesEnabled ? "enabled" : "disabled");
            sb.Append(", ");
            sb.Append(ticket.factoriesActual.ToString("#,0"));
            sb.Append(" of ");
            sb.Append(ticket.factoriesNeeded.ToString("#,0"));

            sb.Append("], demand[");
            sb.Append(ticket.demandText);
            sb.Append("]");

            logger_.debug(sb.ToString());
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void checkDirty()
      {
         try
         {
            StringBuilder sb = new StringBuilder("Hubberly");
            if (!string.IsNullOrWhiteSpace(world_.filename))
            {
               sb.Append(" - ");
               sb.Append(world_.filename);
            }
            sb.Append(world_.dirty ? " *" : string.Empty);
            Text = sb.ToString();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void fileToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
      {
         try
         {
            saveToolStripMenuItem.Enabled = world_.dirty;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void saveToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            if (string.IsNullOrWhiteSpace(world_.filename))
            {
               saveAsToolStripMenuItem_Click(null, null);
            }
            else
            {
               world_.save();
               checkDirty();
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            SaveFileDialog dlg = new SaveFileDialog();
            dlg.Title = "Please tell me where to save the world";
            dlg.AddExtension = true;
            dlg.OverwritePrompt = true;
            dlg.Filter = "XML Files (*.xml)|*.xml|All Files (*.*)|*.*";
            DialogResult result = dlg.ShowDialog(this);
            if (DialogResult.OK == result)
            {
               mruFile_.addFile(dlg.FileName);
               world_.saveAs(mruFile_.firstFilename);
               checkDirty();
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void load(string filename)
      {
         try
         {
            // Clear the existing definitions
            world_.clear();

            // Load the new file
            mruFile_.addFile(filename);
            if (!world_.load(filename))
            {
               string msg = string.Format("failed to load archive '{0}'", filename);
               MessageBox.Show(this, msg, "Hubberly - load failure", MessageBoxButtons.OK);
            }
            checkDirty();

            // And fill the display with the new data
            refreshConvoyCombo();

            rebuildConvoyTab();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void rebuildConvoyTab()
      {
         try
         {
            //logger_.debug("rebuildConvoyTab()");

            // clearing the items will not invoke the selectIndexChanged() event
            // so we clear the selected collections explicitly
            lvConvoyNames.SelectedItems.Clear();
            lvConvoyCities.SelectedItems.Clear();
            lvConvoyNames.Items.Clear();
            world_.convoys.OfType<KeyValuePair<string, Convoy>>().All(kvp =>
            {
               lvConvoyNames.Items.Add(kvp.Key).Tag = kvp.Value;
               return true;
            });
            if (0 < lvConvoyNames.Items.Count)
            {
               lvConvoyNames.Enabled = true;
               lvConvoyNames.Items[0].Selected = true;
            }
            else
            {
               lvConvoyNames.Enabled = false;
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void clear()
      {
         try
         {
            // Clear the existing definitions
            world_.clear();
            checkDirty();
            refreshProductionTab();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void openToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            if (!checkSaveDirty())
            {
               return;
            }
            OpenFileDialog dlg = new OpenFileDialog();
            dlg.Title = "Please select the new world to open";
            dlg.Multiselect = false;
            dlg.AddExtension = true;
            dlg.Filter = "XML Files (*.xml)|*.xml|All Files (*.*)|*.*";
            DialogResult result = dlg.ShowDialog(this);
            if (DialogResult.OK == result)
            {
               load(dlg.FileName);
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void newToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            if (!checkSaveDirty())
            {
               return;
            }
            clear();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      void handleClicked(object sender, Utilities.MRUFile.ClickedEventArgs eventArgs)
      {
         try
         {
            if (checkSaveDirty())
            {
               load(eventArgs.filename);
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private bool checkSaveDirty()
      {
         try
         {
            if ((null != world_) && world_.dirty)
            {
               DialogResult msgBox = MessageBox.Show(this, "Do you want to save the changes?",
                  "Hubberly - there are unsaved modifications", MessageBoxButtons.YesNoCancel);
               if (DialogResult.Cancel == msgBox)
               {
                  return false;
               }
               else if (DialogResult.Yes == msgBox)
               {
                  saveToolStripMenuItem_Click(null, null);
               }
            }
            // OK to carry on ...
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         return false;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void Main_FormClosing(object sender, FormClosingEventArgs e)
      {
         try
         {
            if (!checkSaveDirty())
            {
               e.Cancel = true;
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void btnSelectAllCities_Click(object sender, EventArgs e)
      {
         try
         {
            lvProdCities.Items.OfType<ListViewItem>().ToList().ForEach(item => item.Selected = true);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void btnSelectNoneCities_Click(object sender, EventArgs e)
      {
         try
         {
            lvProdCities.Items.OfType<ListViewItem>().ToList().ForEach(item => item.Selected = false);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void chkGroupByHub_CheckedChanged(object sender, EventArgs e)
      {
         try
         {
            lvProdCities.ShowGroups = Program.settings.groupByHub = chkGroupBySupply.Checked;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void checkConvoyButtons()
      {
         try
         {
            btnAddPort.Enabled = (0 < lvConvoyNames.SelectedItems.Count);
            btnRemovePort.Enabled = (0 < lvConvoyPorts.SelectedItems.Count);

            int portCount = lvConvoyPorts.Items.Count, gapCheck = 0;
            
            // Can move up if there's a gap above the current selection
            // i.e. the selected indices are not contiguous
            btnPortUp.Enabled = (0 < portCount) &&
               lvConvoyPorts.SelectedIndices.OfType<int>().ToList()
               .Any<int>(index => index != gapCheck++);
            
            // Can move down if there's a gap below the current selection
            gapCheck = portCount - 1;
            btnPortDown.Enabled = (1 < portCount) &&
               lvConvoyPorts.SelectedIndices.OfType<int>().ToList()
                  .Reverse<int>().Any(index => index != gapCheck--);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void btnAddConvoy_Click(object sender, EventArgs e)
      {
         try
         {
            DialogGetString dlgName = new DialogGetString();
            dlgName.title = "Name of the convoy";
            dlgName.label = "&Name:";
            dlgName.checkValue = ((text) => !world_.convoys.ContainsKey(text));
            if (DialogResult.OK == dlgName.ShowDialog(this))
            {
               Convoy convoy = new Convoy(world_);
               convoy.name = dlgName.result;
               convoy.dirty = true;

               ListViewItem item = new ListViewItem();
               item.Text = convoy.name;
               item.Tag = convoy;
               lvConvoyNames.Items.Add(item);
               
               if (1 == lvConvoyNames.Items.Count)
               {
                  item.Selected = true;
               }
               checkDirty();
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private Convoy selectedConvoy
      {
         get
         {
            if (1 != lvConvoyNames.SelectedItems.Count)
            {
               // None-are or more-than-one-is selected
               return null;
            }
            return lvConvoyNames.SelectedItems[0].Tag as Convoy;
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private List<Convoy> selectedConvoys
      {
         get
         {
            if (0 == lvConvoyNames.SelectedItems.Count)
            {
               return null;
            }
            return lvConvoyNames.SelectedItems.OfType<ListViewItem>()
               .Select<ListViewItem, Convoy>(item => item.Tag as Convoy).ToList();
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private List<City> selectedPorts
      {
         get
         {
            if (0 == lvConvoyPorts.SelectedItems.Count)
            {
               return null;
            }
            return lvConvoyPorts.SelectedItems.OfType<ListViewItem>()
               .Select<ListViewItem, City>(item => item.Tag as City).ToList();
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void worldInfoToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            OpenFileDialog dlgFilename = new OpenFileDialog();
            dlgFilename.AddExtension = true;
            dlgFilename.CheckFileExists = true;
            dlgFilename.DefaultExt = ".txt";
            dlgFilename.FileName = "worldinfo.txt";
            dlgFilename.Filter = "WorldInfo file (worldinfo.txt)|worldinfo.txt|All Files (*.)|*.*";
            dlgFilename.InitialDirectory = Path.Combine(Environment.ExpandEnvironmentVariables("%APPDATA%"),
               "Kalypso Media", "Patrician 4 Addon");
            dlgFilename.Multiselect = false;
            dlgFilename.Title = "Where is the WorldInfo.Txt file on your PC?";
            DialogResult result = dlgFilename.ShowDialog(this);
            if (DialogResult.OK == result)
            {
               Program.settings.worldFilename = dlgFilename.FileName;
               resyncWorldInfo();
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      //
      private void configurationToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
      {
         try
         {
            string filename = Program.settings.worldFilename;
            refreshToolStripMenuItem.Enabled = File.Exists(filename ?? string.Empty);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void refreshToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            resyncWorldInfo();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private static Logger logger_ = new Logger(typeof(Main));
      private World world_;
      private Utilities.MRUFile mruFile_;
      private List<TradeTicket> tradeTickets_;
      private const string groupConvoyHub_ = "Hub City";
      private const string groupInConvoy_ = "In a convoy";
      private const string groupNotSupplied_ = "Not in a convoy";
      private const int warehouseCapacity_ = 2000;
   }
}
